會想要研究這個小project,是因為在實作project ECC
時,想要動態傳遞ttl
。原先以為不太難,很快可以搞定,可是在玩了一陣子之後發現,當中有不少有趣的地方,所以想要有系統地做成筆記。
postman
是一個decorator factory
,其接受一個item
作為參數。wrapper
內有一個print
來幫忙確認傳進來的item
值。
# postman.py
from functools import wraps
def postman(item):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
print(f'{item=} is received')
return fn(*args, **kwargs)
return wrapper
return decorator
我們的目標是研究,有幾種方法可以動態傳遞item
給postman
,並裝飾(更精準的說法應該是apply
)於# 00
中Class
的func
上。
# 00
from postman import postman
class Class:
def func(self):
...
理論上,Class
的func
應該是non-data descriptor instance
,但是為方便描述,我們將稱呼其為non-data descriptor
。
instance.__dict__
func
為non-data descriptor
,其由instance
存取時,會返回由types.MethodType
生成的bound method
,所以__init__
中一開始的print(type(self.func))
為method
。postman(item)
會返回帶有item
資訊的decorator
function
,所以postman(item)(self.func)
相當於decorator(self.func)
,會返回一個帶有item
資訊的wrapper
function
。func
為non-data descriptor
,其沒有__set__
,所以self.func = postman(item)(self.func)
相當於將帶有item
資訊的wrapper
function
存於inst.__dict__
中。此時觀察print(type(self.func))
可以發現其已經變為function
。inst.func
時,相當於呼叫wrapper
。其會呼叫self.func
這個bound method
後回傳。由於self.func
是bound method
,self
會自己傳遞,所以inst.func
使用起來就像是一般的instance method
。func
由inst.__dict__
中移除,我們還是可以使用inst.func()
的語法來呼叫原來的func
。# 01
from postman import postman
class Class:
def __init__(self, item):
print(type(self.func)) # method
self.func = postman(item)(self.func)
print(type(self.func)) # function
def func(self):
...
if __name__ == '__main__':
inst = Class('xmas_card')
inst.func() # item='xmas_card' is received
print(vars(inst)) # {'func': <function Class.func at 0x0000023BF70C6840>}
del vars(inst)['func']
inst.func() # nothing shown on the screen
雖然方法1
只有短短幾行,但中間發生了很多事情。我們認為這是個hacky
的方法,production code
可能不適合這麼寫。
__new__
方法2
與方法1
類似,只是我們改在__new__
來做。
# 02
from postman import postman
class Class:
def __new__(cls, item):
inst = super().__new__(cls)
inst.func = postman(item)(inst.func)
return inst
def func(self):
...
if __name__ == '__main__':
inst = Class('xmas_card')
inst.func() # item='xmas_card' is received
__new__
方法3
於生成instance
前,先使用了cls.func = postman(item)(cls.func)
後,再生成instance
。
這麼做有個大問題,而且如果只利用Class
生成一個instance
的話,或許還觀察不出來。由於__new__
是「每次」Class
需要生成instance
的時候,都會被呼叫一次,所以相當於每次生成instance
,都會做一次cls.func = postman(item)(cls.func)
。
# 03
中我們只呼叫了一次inst.func
,卻看到三個訊息被印出,因為 Class('xmas_card')
、 Class('mail')
及Class('package')
分別於__new__
中都mutate
了一次func
。
# 03
from postman import postman
class Class:
def __new__(cls, item):
cls.func = postman(item)(cls.func)
inst = super().__new__(cls)
return inst
def func(self):
...
if __name__ == '__main__':
Class('xmas_card'), Class('mail')
inst = Class('package')
inst.func()
# item='package' is received
# item='mail' is received
# item='xmas_card' is received
方法4
我們寫了一個名為dec
的decorator
,來裝飾在Class
上。
dec
接受一個item
參數後,返回一個wrapper
function
。wrapper
接收一個cls
參數,我們使用vars(cls).get(name)
看看能不能從cls.__dict__
中取到func
。如果有取到的話,再進一步判斷其是否為callable
。如果是的話,使用setattr
重新設定cls.name
為postman(item)(obj))
。由於我們mutate
了cls
,所以不管是在mutate
前或後生成的instance
都將會受影響。mutate
的語法可以直接使用dec('xmas_card')(Class)
,不過如果想寫成Class = dec('xmas_card')(Class)
或是於Class
上加上@dec('xmas_card')
也是可以的。
# 04
from postman import postman
def dec(item):
def wrapper(cls):
name = 'func'
if obj := vars(cls).get(name):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
return wrapper
class Class:
def func(self):
...
if __name__ == '__main__':
inst = Class()
dec('xmas_card')(Class)
inst2 = Class()
inst2.func() # item='xmas_card' is received
inst.func() # item='xmas_card' is received
由於我們是針對vars(cls)
來進行搜尋,所以如# 04a
中func
定義於ParentClass
而非Class
的情況,此方法並不適用。
# 04a
...
class ParentClass:
def func(self):
...
class Class(ParentClass):
pass
if __name__ == '__main__':
inst = Class()
dec('xmas_card')(Class)
inst.func() # nothing shown on the screen
方法5
與方法4
類似,但我們使用getattr
來尋找cls.name
。
# 05
from postman import postman
def dec(item):
def wrapper(cls):
name = 'func'
if obj := getattr(cls, name, None):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
return wrapper
class Class:
def func(self):
...
if __name__ == '__main__':
inst = Class()
dec('xmas_card')(Class) # Class is mutated
inst.func() # item='xmas_card' is received
由於這是class
級別的lookup
(可以參考[Day18]),所以如# 05a
中func
定義於ParentClass
而非Class
的情況,此方法也可適用。
# 05a
...
class ParentClass:
def func(self):
...
class Class(ParentClass):
pass
if __name__ == '__main__':
inst = Class()
dec('xmas_card')(Class)
inst.func() # item='xmas_card' is received
方法6
使用了與方法4
類似的邏輯,只是這次使用了metaclass
而不是decorator
。此時item
需要於Class
生成時,以keyword-only argument
傳遞,並需要記得於Meta.__new__
中加入item
的signature
,才能存取到item
。
# 06
from postman import postman
class Meta(type):
def __new__(mcls, cls_name, cls_bases, cls_dict, item):
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
name = 'func'
if obj := vars(cls).get(name):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
item = 'xmas_card'
class Class(metaclass=Meta, item=item):
def func(self):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' is received
與# 04a
一樣,# 06a
這種繼承的情況也不適用。
# 06a
...
item = 'xmas_card'
class ParentClass:
def func(self):
...
class Class(ParentClass, metaclass=Meta, item=item):
...
if __name__ == '__main__':
inst = Class()
inst.func() # nothing shown on the screen
方法7
與方法6
類似,但我們使用getattr
來尋找cls.name
。
# 07
from postman import postman
class Meta(type):
def __new__(mcls, cls_name, cls_bases, cls_dict, item):
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
name = 'func'
if obj := getattr(cls, name, None):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
item = 'xmas_card'
class Class(metaclass=Meta, item=item):
def func(self):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' is received
與# 05a
一樣,# 07a
這種繼承的情況也適用。
# 07a
...
item = 'xmas_card'
class ParentClass:
def func(self):
...
class Class(ParentClass, metaclass=Meta, item=item):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' is received
方法8
與方法6
的Meta
是一樣的,但我們另外建立了一個send_item
function
。
send_item
接收一個參數item
,並返回一個wrapper
function
。wrapper
function
接收一個參數cls
,並複製所有cls
的資訊加上於send_item
傳入的item
作為Meta
的參數,來生成一個全新的class
返回。# 08
from postman import postman
class Meta(type):
def __new__(mcls, cls_name, cls_bases, cls_dict, item):
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
name = 'func'
if obj := vars(cls).get(name):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
def send_item(item):
def wrapper(cls):
return Meta(cls.__name__, cls.__bases__, dict(vars(cls)), item=item)
return wrapper
item = 'xmas_card'
@send_item(item)
class Class:
def func(self):
...
if __name__ == '__main__':
inst = Class() # type(Class) => <class '__main__.Meta'>
inst.func() # item='xmas_card' is received
與# 04a
與#06a
一樣,# 08a
這種繼承的情況也不適用。
# 08a
...
class ParentClass:
def func(self):
...
@send_item(item)
class Class(ParentClass):
...
if __name__ == '__main__':
inst = Class()
inst.func() # nothing shown on the screen
方法9
與方法8
類似,但我們使用getattr
來尋找cls.name
。
# 09
from postman import postman
class Meta(type):
def __new__(mcls, cls_name, cls_bases, cls_dict, item):
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
name = 'func'
if obj := getattr(cls, name, None):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
def send_item(item):
def wrapper(cls):
return Meta(cls.__name__, cls.__bases__, dict(vars(cls)), item=item)
return wrapper
item = 'xmas_card'
@send_item(item)
class Class:
def func(self):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' is received
與# 05a
與# 07a
一樣,# 09a
這種繼承的情況也適用。
# 09a
...
class ParentClass:
def func(self):
...
@send_item(item)
class Class(ParentClass):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' is received
方法10
直接建立一個function
,接收cls
及item
兩個參數後,直接於function
中mutate
cls
,重新將cls.name
指定為postman(item)(obj)
後,返回cls
。
# 10
from postman import postman
def send_item(cls, item):
name = 'func'
if obj := vars(cls).get(name):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
class Class:
def func(self):
...
if __name__ == '__main__':
send_item(Class, 'xmas_card')
inst = Class()
inst.func() # item='xmas_card' is received
# 10a
這種繼承的情況也不適用。
# 10a
...
item = 'xmas_card'
class ParentClass:
def func(self):
...
class Class(ParentClass):
...
if __name__ == '__main__':
send_item(Class, 'xmas_card')
inst = Class()
inst.func() # nothing shown on the screen
方法11
與方法10
類似,但我們使用getattr
來尋找cls.name
。
# 11
from postman import postman
def send_item(cls, item):
name = 'func'
if obj := getattr(cls, name, None):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
class Class:
def func(self):
...
if __name__ == '__main__':
send_item(Class, 'xmas_card')
inst = Class()
inst.func() # item='xmas_card' is received
# 11a
這種繼承的情況也適用。
# 11a
...
item = 'xmas_card'
class ParentClass:
def func(self):
...
class Class(ParentClass):
...
if __name__ == '__main__':
send_item(Class, 'xmas_card')
inst = Class()
inst.func() # item='xmas_card' is received
方法12
為將item
設定於class body
中作為class variable
。
# 12
from postman import postman
class Class:
item = 'xmas_card'
@postman(item)
def func(self):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' reveived
方法13
為將item
設定於global scope
中。
# 13
from postman import postman
item = 'xmas_card'
class Class:
@postman(item)
def func(self):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' reveived
function
中。如果是要建立一個尚不存在的function
,經裝飾後再設為cls
或self
的attribute
的話,可能會需要types.MethodType
的幫忙。function
的name
(func
)是已知的,其實我們可以考慮將需要裝飾的name(s)
作為一個container
收集起來,也當作參數與item
一起傳遞到decorator
或Meta
內,再使用迴圈處理。